import { Precision } from '../Precision';

/**
* @description 二维空间坐标
* @author xuld
* @date 2024-11-08 17:39:13
* @lastEditTime 2024-11-08 17:39:13
* @lastEditors xuld
*/

export class XY {
    public x: number;
    public y: number;

    constructor(x: number = 0, y: number = 0) {
        this.x = x;
        this.y = y;
    }

    /**
     * @description 验证参数是否为有效数字
     * @throws 当参数无效时抛出错误
     */
    private validateNumbers(...args: number[]): void {
        for (const arg of args) {
            if (typeof arg !== 'number' || isNaN(arg) || !isFinite(arg)) {
                throw new Error('Invalid number parameter');
            }
        }
    }

    /**
     * @description 设置坐标值
     * @param x - X坐标值
     * @param y - Y坐标值
     * @returns 当前对象
     */
    public set(x: number, y: number): XY {
        this.validateNumbers(x, y);
        this.x = x;
        this.y = y;
        return this;
    }

    /**
     * @description 计算向量的模长
     * @returns 向量的模长
     */
    public modulus(): number {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    }

    /**
     * @description 计算向量模长的平方
     * @returns 向量模长的平方
     */
    public squareModulus(): number {
        return this.x * this.x + this.y * this.y;
    }

    /**
     * @description 归一化向量，返回新向量
     * @param result - 可选的结果对象，用于存储结果
     * @returns 归一化后的新向量
     */
    public normalize(result?: XY): XY {
        result ||= new XY();
        result.set(this.x, this.y).normalizeSelf();
        return result;
    }

    /**
     * @description 对自身进行归一化
     */
    public normalizeSelf(): void {
        let d = this.modulus();
        if (d < Precision.EPSILON10) {
            this.x = 0;
            this.y = 0;
        }
        else {
            this.x = this.x / d;
            this.y = this.y / d;
        }
    }

    /**
     * @description 判断两个点是否相等
     * @param other - 要比较的点
     * @param tolerance - 容差值，默认为 EPSILON4
     * @returns 是否相等
     */
    public equals(other: XY, tolerance: number = Precision.EPSILON4): boolean {
        if (Math.abs(this.x - other.x) > tolerance) return false;
        if (Math.abs(this.y - other.y) > tolerance) return false;
        return true;
    }

    /**
     * @description 向量加法，返回新向量
     * @param other - 要相加的向量
     * @returns 相加后的新向量
     */
    public add(other: XY): XY {
        return new XY(this.x + other.x, this.y + other.y);
    }

    /**
     * @description 向量加法，结果存储在自身
     * @param other - 要相加的向量
     */
    public addSelf(other: XY): void {
        this.x += other.x;
        this.y += other.y;
    }

    /**
     * @description 向量减法，返回新向量
     * @param other - 要相减的向量
     * @param result - 可选的结果对象，用于存储结果
     * @returns 相减后的新向量
     */
    public subtract(other: XY, result?: XY): XY {
        result ||= new XY(this.x, this.y);
        result.subtractSelf(other);
        return result;
    }

    /**
     * @description 向量减法，结果存储在自身
     * @param other - 要相减的向量
     */
    public subtractSelf(other: XY): void {
        this.x -= other.x;
        this.y -= other.y;
    }

    /**
     * @description 向量乘以标量，返回新向量
     * @param scalar - 标量值
     * @param result - 可选的结果对象，用于存储结果
     * @returns 相乘后的新向量
     */
    public multiply(scalar: number, result?: XY): XY {
        result ||= new XY(this.x, this.y);
        result.multiplySelf(scalar);
        return result;
    }

    /**
     * @description 向量乘以标量，结果存储在自身
     * @param scalar - 标量值
     */
    public multiplySelf(scalar: number): void {
        this.x *= scalar;
        this.y *= scalar;
    }

    /**
     * @description 向量除以标量，返回新向量
     * @param scalar - 标量值
     * @param result - 可选的结果对象，用于存储结果
     * @returns 相除后的新向量
     * @throws 当除数为0时抛出错误
     */
    public divide(scalar: number, result?: XY): XY {
        result ||= new XY(this.x, this.y);
        result.divideSelf(scalar);
        return result;
    }

    /**
     * @description 向量除以标量，结果存储在自身
     * @param scalar - 标量值
     * @throws 当除数为0时抛出错误
     */
    public divideSelf(scalar: number): void {
        if (scalar === 0) {
            throw new Error("Division by zero");
        }
        this.x /= scalar;
        this.y /= scalar;
    }

    /**
     * @description 计算两个向量的点积
     * @param other - 另一个向量
     * @returns 点积结果
     */
    public dot(other: XY): number {
        return this.x * other.x + this.y * other.y;
    }

    /**
     * @description 计算两个向量的叉积
     * @param other - 另一个向量
     * @returns 叉积结果
     */
    public cross(other: XY): number {
        return this.x * other.y - this.y * other.x;
    }

    /**
     * @description 向量取反，返回新向量
     * @returns 取反后的新向量
     */
    public negate(): XY {
        return new XY(-this.x, -this.y);
    }

    /**
     * @description 向量取反，结果存储在自身
     */
    public negateSelf(): void {
        this.x = -this.x;
        this.y = -this.y;
    }

    /**
     * @description 从其他向量复制坐标值
     * @param other - 要复制的向量
     */
    public copyFrom(other: XY): void {
        this.x = other.x;
        this.y = other.y;
    }

    /**
     * @description 点绕中心点旋转
     * @param radian - 旋转弧度
     * @param center - 旋转中心点，默认为原点(0,0)
     * @param res - 可选的结果对象，用于存储结果
     * @returns 旋转后的新向量
     */
    public rotate(radian: number, center?: XY, res?: XY): XY {
        res ||= new XY(this.x, this.y);

        if (radian % (Math.PI * 2) === 0) {
            return res;
        }

        // 缓存三角函数计算结果
        const c: number = Math.cos(radian);
        const s: number = Math.sin(radian);

        let x = this.x;
        let y = this.y;

        if (center) {
            x -= center.x;
            y -= center.y;
        }

        res.x = c * x - s * y;
        res.y = s * x + c * y;

        if (center) {
            res.x += center.x;
            res.y += center.y;
        }

        return res;
    }

    /**
     * @description 点自身绕中心点旋转
     * @param radian - 旋转弧度
     * @param center - 旋转中心点，默认为原点(0,0)
     */
    public rotateSelf(radian: number, center?: XY): void {
        this.rotate(radian, center, this)
    }

    /**
     * @description 创建当前对象的副本
     * @returns 新的 XY 对象
     */
    public clone(): XY {
        return new XY(this.x, this.y);
    }

    /**
     * @description 转换为字符串表示
     * @returns 格式化的坐标字符串
     */
    public toString(): string {
        return `XY(${this.x}, ${this.y})`;
    }

    /**
     * @description 从数组创建 XY 对象
     * @param array - 包含 x,y 坐标的数组
     * @param offset - 数组起始偏移量，默认为0
     * @returns 新的 XY 对象
     */
    public static fromArray(array: number[], offset: number = 0): XY {
        if (!array || array.length < offset + 2) {
            throw new Error("Invalid array length");
        }
        return new XY(array[offset], array[offset + 1]);
    }

    /**
     * @description 将坐标转换为数组
     * @param target - 目标数组，如果不提供则创建新数组
     * @param offset - 数组起始偏移量，默认为0
     * @returns 包含坐标的数组
     */
    public toArray(target: number[] = [], offset: number = 0): number[] {
        if (offset < 0) {
            throw new Error("Invalid offset");
        }
        target[offset] = this.x;
        target[offset + 1] = this.y;
        return target;
    }

    /**
     * @description 计算两点之间距离
     * @param other - 另一个点
     * @returns 距离
     */
    public distanceTo(other: XY): number {
        const dx = this.x - other.x;
        const dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    /**
     * @description 判断向量是否为零向量
     * @param tolerance - 容差值，默认为 EPSILON10
     * @returns 是���为零向量
     */
    public isZero(tolerance: number = Precision.EPSILON10): boolean {
        return Math.abs(this.x) < tolerance && Math.abs(this.y) < tolerance;
    }

    /**
     * @description 线性插值，返回新向量
     * @param other - 另一个点
     * @param t - 插值系数，取值范围为[0,1]
     * @param result - 可选的结果对象，用于存储结果
     * @returns 插值后的新向量
     */
    public lerp(other: XY, t: number, result?: XY): XY {
        result ||= new XY();
        result.x = this.x + (other.x - this.x) * t;
        result.y = this.y + (other.y - this.y) * t;
        return result;
    }

    /**
     * @description 获取向量夹角
     * @param other - 另一个向量
     * @returns 夹角弧度
     */
    public angleTo(other: XY): number {
        return Math.atan2(this.cross(other), this.dot(other));
    }
}